1use std::{
2 collections::{HashMap, HashSet},
3 sync::{
4 Arc, Mutex, Weak,
5 atomic::{AtomicU64, Ordering},
6 },
7 thread,
8 time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_info};
12use arc_swap::ArcSwapOption;
13use ffmpeg_next as ffmpeg;
14use thiserror::Error as ThisError;
15
16use super::video_layer::{
17 self, LayerGeometry, MAX_DIMENSION, VideoFit, VideoInputId, VideoLayer, VideoLayerError,
18 VideoRect,
19};
20use crate::{
21 buffer::MediaBuffer,
22 bus::{Bus, BusEvent},
23 color::Color,
24 control::{ControlMsg, ControlReceiver, drain_control},
25 element::{Element, ElementType, Sink, Source, SourceElement, element_pp_log},
26 error::Result,
27 pad::SrcPad,
28 pool::{UnboundObjectPool, UnboundObjectPoolRef},
29 schedule::PeriodicSchedule,
30};
31
32const OUTPUT_POOL_SIZE: usize = 4;
33const CONTROL_POLL_INTERVAL: Duration = Duration::from_millis(5);
34
35#[derive(Debug, Clone, Copy)]
38pub struct VideoCompositorOptions {
39 pub width: u32,
40 pub height: u32,
41 pub frame_rate: ffmpeg::Rational,
42 pub background: Color,
43}
44
45impl Default for VideoCompositorOptions {
46 fn default() -> Self {
47 Self {
48 width: 1920,
49 height: 1080,
50 frame_rate: ffmpeg::Rational::new(30, 1),
51 background: Color::BLACK,
52 }
53 }
54}
55
56#[derive(Debug, ThisError)]
58pub enum VideoCompositorError {
59 #[error("ffmpeg error: {0}")]
60 Ffmpeg(#[from] ffmpeg::Error),
61
62 #[error(
63 "invalid output dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
64 )]
65 InvalidOutputDimensions { width: u32, height: u32 },
66
67 #[error("invalid frame rate {0}; numerator and denominator must both be positive")]
68 InvalidFrameRate(ffmpeg::Rational),
69
70 #[error(
71 "invalid layer dimensions {width}x{height}; each dimension must be 1..={MAX_DIMENSION}"
72 )]
73 InvalidLayerDimensions { width: u32, height: u32 },
74
75 #[error("layer opacity must be finite and between 0.0 and 1.0, got {0}")]
76 InvalidOpacity(f32),
77
78 #[error("input frame has invalid dimensions {width}x{height}")]
79 InvalidInputDimensions { width: u32, height: u32 },
80
81 #[error("scaled layer would exceed {MAX_DIMENSION}px: {width}x{height}")]
82 ScaledLayerTooLarge { width: u32, height: u32 },
83
84 #[error("the compositor input has been removed")]
85 SourceRemoved,
86
87 #[error(
88 "VideoCompositorInputSink only accepts decoded Video frames, got a {0}; link it after a decoder or video source"
89 )]
90 UnsupportedBuffer(&'static str),
91
92 #[error("VideoCompositor doesn't support seeking a live composition")]
93 SeekUnsupported,
94}
95
96struct VideoInput {
97 id: VideoInputId,
98 latest_frame: ArcSwapOption<UnboundObjectPoolRef<ffmpeg::frame::Video>>,
102 layer: Mutex<VideoLayer>,
105}
106
107struct CompositorShared {
108 inputs: Mutex<HashMap<Arc<str>, Arc<VideoInput>>>,
109 next_input_id: AtomicU64,
110}
111
112#[derive(Clone)]
116pub struct VideoCompositorHandle {
117 shared: Weak<CompositorShared>,
118}
119
120pub struct VideoCompositorInput {
124 pub sink: Box<dyn Sink>,
125 pub layer: VideoLayerHandle,
126}
127
128impl VideoCompositorHandle {
129 pub fn add_source(
133 &self,
134 name: impl Into<String>,
135 layer: VideoLayer,
136 ) -> std::result::Result<Option<VideoCompositorInput>, VideoCompositorError> {
137 validate_layer(layer)?;
138 let Some(shared) = self.shared.upgrade() else {
139 return Ok(None);
140 };
141 let name: Arc<str> = name.into().into();
142 let id = VideoInputId(shared.next_input_id.fetch_add(1, Ordering::Relaxed));
143 let input = Arc::new(VideoInput {
144 id,
145 latest_frame: ArcSwapOption::empty(),
146 layer: Mutex::new(layer),
147 });
148 shared
149 .inputs
150 .lock()
151 .unwrap()
152 .insert(name.clone(), input.clone());
153
154 Ok(Some(VideoCompositorInput {
155 sink: Box::new(VideoCompositorInputSink {
156 name: name.clone(),
157 pp_log: element_pp_log(ElementType::VideoCompositor, &name, None),
158 shared: self.shared.clone(),
159 input: Arc::downgrade(&input),
160 }),
161 layer: VideoLayerHandle {
162 id,
163 name,
164 input: Arc::downgrade(&input),
165 },
166 }))
167 }
168
169 pub fn remove_source(&self, name: &str) {
172 if let Some(shared) = self.shared.upgrade() {
173 shared.inputs.lock().unwrap().remove(name);
174 }
175 }
176
177 pub fn source_count(&self) -> usize {
178 self.shared
179 .upgrade()
180 .map(|shared| shared.inputs.lock().unwrap().len())
181 .unwrap_or(0)
182 }
183}
184
185#[derive(Clone)]
188pub struct VideoLayerHandle {
189 id: VideoInputId,
190 name: Arc<str>,
191 input: Weak<VideoInput>,
192}
193
194impl VideoLayerHandle {
195 pub fn id(&self) -> VideoInputId {
196 self.id
197 }
198
199 pub fn name(&self) -> Arc<str> {
200 self.name.clone()
201 }
202
203 pub fn layer(&self) -> Option<VideoLayer> {
204 self.input
205 .upgrade()
206 .map(|input| *input.layer.lock().unwrap())
207 }
208
209 pub fn set_layer(&self, layer: VideoLayer) -> std::result::Result<(), VideoCompositorError> {
210 validate_layer(layer)?;
211 self.update(|current| *current = layer)
212 }
213
214 pub fn set_rect(&self, rect: VideoRect) -> std::result::Result<(), VideoCompositorError> {
215 validate_rect(rect)?;
216 self.update(|layer| layer.rect = rect)
217 }
218
219 pub fn set_opacity(&self, opacity: f32) -> std::result::Result<(), VideoCompositorError> {
220 validate_opacity(opacity)?;
221 self.update(|layer| layer.opacity = opacity)
222 }
223
224 pub fn set_z_index(&self, z_index: i32) -> std::result::Result<(), VideoCompositorError> {
225 self.update(|layer| layer.z_index = z_index)
226 }
227
228 pub fn set_visible(&self, visible: bool) -> std::result::Result<(), VideoCompositorError> {
229 self.update(|layer| layer.visible = visible)
230 }
231
232 pub fn set_fit(&self, fit: VideoFit) -> std::result::Result<(), VideoCompositorError> {
233 self.update(|layer| layer.fit = fit)
234 }
235
236 fn update(
237 &self,
238 update: impl FnOnce(&mut VideoLayer),
239 ) -> std::result::Result<(), VideoCompositorError> {
240 let input = self
241 .input
242 .upgrade()
243 .ok_or(VideoCompositorError::SourceRemoved)?;
244 update(&mut input.layer.lock().unwrap());
245 Ok(())
246 }
247}
248
249pub struct VideoCompositorInputSink {
254 pp_log: PpLog,
255 name: Arc<str>,
256 shared: Weak<CompositorShared>,
257 input: Weak<VideoInput>,
258}
259
260impl VideoCompositorInputSink {
261 fn detach(&self) {
262 let (Some(shared), Some(input)) = (self.shared.upgrade(), self.input.upgrade()) else {
263 return;
264 };
265 let mut inputs = shared.inputs.lock().unwrap();
266 let is_current = inputs
267 .get(&self.name)
268 .is_some_and(|current| Arc::ptr_eq(current, &input));
269 if is_current {
270 inputs.remove(&self.name);
271 }
272 }
273}
274
275impl Element for VideoCompositorInputSink {
276 fn name(&self) -> Arc<str> {
277 self.name.clone()
278 }
279
280 fn element_type(&self) -> ElementType {
281 ElementType::VideoCompositor
282 }
283
284 fn pp_log(&self) -> &PpLog {
285 &self.pp_log
286 }
287
288 fn pp_log_mut(&mut self) -> &mut PpLog {
289 &mut self.pp_log
290 }
291}
292
293impl Sink for VideoCompositorInputSink {
294 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
295 let Some(input) = self.input.upgrade() else {
296 return Ok(());
297 };
298 match buf {
299 MediaBuffer::Video(frame) => {
300 input.latest_frame.store(Some(frame));
301 Ok(())
302 }
303 MediaBuffer::Eos => {
304 self.detach();
305 Ok(())
306 }
307 MediaBuffer::Packet(_) => Err(VideoCompositorError::UnsupportedBuffer("Packet").into()),
308 MediaBuffer::Audio(_) => Err(VideoCompositorError::UnsupportedBuffer("Audio").into()),
309 }
310 }
311
312 fn control(&mut self, msg: ControlMsg) -> Result<()> {
313 match msg {
314 ControlMsg::Stop => self.detach(),
315 ControlMsg::Seek(_) => {
316 if let Some(input) = self.input.upgrade() {
317 input.latest_frame.store(None);
318 }
319 }
320 ControlMsg::Pause | ControlMsg::Resume => {}
321 }
322 Ok(())
323 }
324}
325
326#[derive(Clone)]
327struct InputSnapshot {
328 id: VideoInputId,
329 layer: VideoLayer,
330 frame: Option<Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>>>,
331}
332
333#[derive(Debug, Clone, Copy, PartialEq, Eq)]
334struct ScaleDefinition {
335 source_format: ffmpeg::format::Pixel,
336 source_width: u32,
337 source_height: u32,
338 target_width: u32,
339 target_height: u32,
340}
341
342struct InputScaler {
343 definition: Option<ScaleDefinition>,
344 context: Option<ffmpeg::software::scaling::Context>,
345 output: ffmpeg::frame::Video,
346}
347
348impl InputScaler {
349 fn new() -> Self {
350 Self {
351 definition: None,
352 context: None,
353 output: ffmpeg::frame::Video::empty(),
354 }
355 }
356
357 fn scale(
358 &mut self,
359 source: &ffmpeg::frame::Video,
360 width: u32,
361 height: u32,
362 ) -> std::result::Result<&ffmpeg::frame::Video, ffmpeg::Error> {
363 let definition = ScaleDefinition {
364 source_format: source.format(),
365 source_width: source.width(),
366 source_height: source.height(),
367 target_width: width,
368 target_height: height,
369 };
370 if self.definition != Some(definition) {
371 self.context = Some(ffmpeg::software::scaling::Context::get(
372 definition.source_format,
373 definition.source_width,
374 definition.source_height,
375 ffmpeg::format::Pixel::BGRA,
376 width,
377 height,
378 ffmpeg::software::scaling::Flags::BILINEAR,
379 )?);
380 self.output = ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height);
381 self.definition = Some(definition);
382 }
383 self.context
384 .as_mut()
385 .expect("created with a changed definition or retained from a matching one")
386 .run(source, &mut self.output)?;
387 Ok(&self.output)
388 }
389}
390
391pub struct VideoCompositor {
401 pp_log: PpLog,
402 name: Arc<str>,
403 shared: Arc<CompositorShared>,
404 options: VideoCompositorOptions,
405 frame_interval: Duration,
406 frame_index: i64,
407 scalers: HashMap<VideoInputId, InputScaler>,
408 output_pool: UnboundObjectPool<ffmpeg::frame::Video>,
409 pad: SrcPad,
410}
411
412unsafe impl Send for VideoCompositor {}
416
417impl VideoCompositor {
418 pub fn new(
419 name: impl Into<String>,
420 options: VideoCompositorOptions,
421 ) -> std::result::Result<(Self, VideoCompositorHandle), VideoCompositorError> {
422 validate_output_options(options)?;
423 let name: Arc<str> = name.into().into();
424 let pp_log = element_pp_log(ElementType::VideoCompositor, &name, None);
425 let shared = Arc::new(CompositorShared {
426 inputs: Mutex::new(HashMap::new()),
427 next_input_id: AtomicU64::new(1),
428 });
429 let frame_interval = Duration::from_secs_f64(
430 options.frame_rate.denominator() as f64 / options.frame_rate.numerator() as f64,
431 );
432 let (width, height) = (options.width, options.height);
433 let output_pool = UnboundObjectPool::new(
434 OUTPUT_POOL_SIZE,
435 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
436 |_| {},
437 );
438 pp_info!(
439 pp_log: &pp_log,
440 "created: {}x{}, frame_rate={}, format=BGRA",
441 width,
442 height,
443 options.frame_rate
444 );
445 Ok((
446 Self {
447 name: name.clone(),
448 pp_log,
449 shared: shared.clone(),
450 options,
451 frame_interval,
452 frame_index: 0,
453 scalers: HashMap::new(),
454 output_pool,
455 pad: SrcPad::new(format!("{name}_src")),
456 },
457 VideoCompositorHandle {
458 shared: Arc::downgrade(&shared),
459 },
460 ))
461 }
462
463 pub fn format(&self) -> ffmpeg::format::Pixel {
464 ffmpeg::format::Pixel::BGRA
465 }
466
467 pub fn width(&self) -> u32 {
468 self.options.width
469 }
470
471 pub fn height(&self) -> u32 {
472 self.options.height
473 }
474
475 pub fn frame_rate(&self) -> ffmpeg::Rational {
476 self.options.frame_rate
477 }
478
479 pub fn time_base(&self) -> ffmpeg::Rational {
480 ffmpeg::Rational::new(
481 self.options.frame_rate.denominator(),
482 self.options.frame_rate.numerator(),
483 )
484 }
485
486 fn snapshots(&self) -> Vec<InputSnapshot> {
487 let inputs: Vec<_> = self
488 .shared
489 .inputs
490 .lock()
491 .unwrap()
492 .values()
493 .cloned()
494 .collect();
495 inputs
496 .into_iter()
497 .map(|input| InputSnapshot {
498 id: input.id,
499 layer: *input.layer.lock().unwrap(),
500 frame: input.latest_frame.load_full(),
501 })
502 .collect()
503 }
504
505 fn compose_frame(
506 &mut self,
507 ) -> std::result::Result<UnboundObjectPoolRef<ffmpeg::frame::Video>, VideoCompositorError> {
508 let mut snapshots = self.snapshots();
509 let active: HashSet<_> = snapshots.iter().map(|snapshot| snapshot.id).collect();
510 self.scalers.retain(|id, _| active.contains(id));
511 snapshots.sort_by(|left, right| {
512 left.layer
513 .z_index
514 .cmp(&right.layer.z_index)
515 .then_with(|| left.id.cmp(&right.id))
516 });
517
518 let mut output = self.output_pool.get();
519 fill_background(&mut output, self.options.background);
520 for snapshot in snapshots {
521 if !snapshot.layer.visible || snapshot.layer.opacity == 0.0 {
522 continue;
523 }
524 let Some(frame) = snapshot.frame else {
525 continue;
526 };
527 let geometry = layer_geometry(
528 frame.width(),
529 frame.height(),
530 snapshot.layer.rect,
531 snapshot.layer.fit,
532 )?;
533 let scaled = self
534 .scalers
535 .entry(snapshot.id)
536 .or_insert_with(InputScaler::new)
537 .scale(&frame, geometry.image_width, geometry.image_height)
538 .map_err(VideoCompositorError::from)?;
539 blend_bgra(&mut output, scaled, geometry, snapshot.layer.opacity);
540 }
541 output.set_pts(Some(self.frame_index));
542 self.frame_index += 1;
543 Ok(output)
544 }
545
546 fn push_frame(&mut self, bus: &Bus) -> std::result::Result<(), VideoCompositorError> {
547 let output = self.compose_frame()?;
548 if let Err(error) = self.pad.push(MediaBuffer::Video(Arc::new(output))) {
549 bus.post(
550 &self.pp_log,
551 BusEvent::Error {
552 element_type: ElementType::VideoCompositor,
553 name: self.name.clone(),
554 error,
555 },
556 );
557 }
558 Ok(())
559 }
560}
561
562impl Element for VideoCompositor {
563 fn name(&self) -> Arc<str> {
564 self.name.clone()
565 }
566
567 fn element_type(&self) -> ElementType {
568 ElementType::VideoCompositor
569 }
570
571 fn pp_log(&self) -> &PpLog {
572 &self.pp_log
573 }
574
575 fn pp_log_mut(&mut self) -> &mut PpLog {
576 &mut self.pp_log
577 }
578}
579
580impl Source for VideoCompositor {
581 fn src_pads(&mut self) -> &mut [SrcPad] {
582 std::slice::from_mut(&mut self.pad)
583 }
584}
585
586impl SourceElement for VideoCompositor {
587 fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> Result<()> {
588 pp_info!(self, "started");
589 let mut schedule = PeriodicSchedule::new(self.frame_interval, Instant::now());
590 loop {
591 let outcome = drain_control(control, self, bus)?;
592 if outcome.stopped {
593 pp_info!(self, "stopped");
594 return Ok(());
595 }
596 if outcome.paused_for > Duration::ZERO {
597 schedule.resume_after_pause(outcome.paused_for, Instant::now());
598 }
599
600 let now = Instant::now();
601 if !schedule.is_due(now) {
602 thread::sleep(schedule.remaining(now).min(CONTROL_POLL_INTERVAL));
603 continue;
604 }
605
606 self.push_frame(bus)?;
607 schedule.advance_after_tick(Instant::now());
608 }
609 }
610
611 fn seek(&mut self, _target: Duration) -> Result<Duration> {
612 Err(VideoCompositorError::SeekUnsupported.into())
613 }
614}
615
616fn validate_output_options(
617 options: VideoCompositorOptions,
618) -> std::result::Result<(), VideoCompositorError> {
619 if options.width == 0
620 || options.height == 0
621 || options.width > MAX_DIMENSION
622 || options.height > MAX_DIMENSION
623 {
624 return Err(VideoCompositorError::InvalidOutputDimensions {
625 width: options.width,
626 height: options.height,
627 });
628 }
629 if options.frame_rate.numerator() <= 0 || options.frame_rate.denominator() <= 0 {
630 return Err(VideoCompositorError::InvalidFrameRate(options.frame_rate));
631 }
632 Ok(())
633}
634
635fn map_layer_error(error: VideoLayerError) -> VideoCompositorError {
641 match error {
642 VideoLayerError::InvalidDimensions { width, height } => {
643 VideoCompositorError::InvalidLayerDimensions { width, height }
644 }
645 VideoLayerError::InvalidOpacity(opacity) => VideoCompositorError::InvalidOpacity(opacity),
646 VideoLayerError::InvalidInputDimensions { width, height } => {
647 VideoCompositorError::InvalidInputDimensions { width, height }
648 }
649 VideoLayerError::ScaledLayerTooLarge { width, height } => {
650 VideoCompositorError::ScaledLayerTooLarge { width, height }
651 }
652 }
653}
654
655fn validate_layer(layer: VideoLayer) -> std::result::Result<(), VideoCompositorError> {
656 video_layer::validate_layer(layer).map_err(map_layer_error)
657}
658
659fn validate_rect(rect: VideoRect) -> std::result::Result<(), VideoCompositorError> {
660 video_layer::validate_rect(rect).map_err(map_layer_error)
661}
662
663fn validate_opacity(opacity: f32) -> std::result::Result<(), VideoCompositorError> {
664 video_layer::validate_opacity(opacity).map_err(map_layer_error)
665}
666
667fn layer_geometry(
668 source_width: u32,
669 source_height: u32,
670 rect: VideoRect,
671 fit: VideoFit,
672) -> std::result::Result<LayerGeometry, VideoCompositorError> {
673 video_layer::layer_geometry(source_width, source_height, rect, fit).map_err(map_layer_error)
674}
675
676fn fill_background(frame: &mut ffmpeg::frame::Video, color: Color) {
677 let width = frame.width() as usize;
678 let height = frame.height() as usize;
679 let stride = frame.stride(0);
680 let data = frame.data_mut(0);
681 for row in 0..height {
682 for pixel in data[row * stride..row * stride + width * 4].chunks_exact_mut(4) {
683 pixel.copy_from_slice(&[color.blue, color.green, color.red, 255]);
684 }
685 }
686}
687
688fn blend_bgra(
689 destination: &mut ffmpeg::frame::Video,
690 source: &ffmpeg::frame::Video,
691 geometry: LayerGeometry,
692 opacity: f32,
693) {
694 let output_width = i64::from(destination.width());
695 let output_height = i64::from(destination.height());
696 let clip_left = i64::from(geometry.clip.x).max(0);
697 let clip_top = i64::from(geometry.clip.y).max(0);
698 let clip_right =
699 (i64::from(geometry.clip.x) + i64::from(geometry.clip.width)).min(output_width);
700 let clip_bottom =
701 (i64::from(geometry.clip.y) + i64::from(geometry.clip.height)).min(output_height);
702 let left = geometry.image_x.max(clip_left);
703 let top = geometry.image_y.max(clip_top);
704 let right = (geometry.image_x + i64::from(geometry.image_width)).min(clip_right);
705 let bottom = (geometry.image_y + i64::from(geometry.image_height)).min(clip_bottom);
706 if left >= right || top >= bottom {
707 return;
708 }
709
710 let source_stride = source.stride(0);
711 let destination_stride = destination.stride(0);
712 let source_data = source.data(0);
713 let destination_data = destination.data_mut(0);
714 for output_y in top..bottom {
715 let source_y = (output_y - geometry.image_y) as usize;
716 let destination_y = output_y as usize;
717 for output_x in left..right {
718 let source_x = (output_x - geometry.image_x) as usize;
719 let destination_x = output_x as usize;
720 let source_offset = source_y * source_stride + source_x * 4;
721 let destination_offset = destination_y * destination_stride + destination_x * 4;
722 let source_pixel = &source_data[source_offset..source_offset + 4];
723 let destination_pixel =
724 &mut destination_data[destination_offset..destination_offset + 4];
725 let alpha = (f32::from(source_pixel[3]) / 255.0) * opacity;
726 let inverse = 1.0 - alpha;
727 for channel in 0..3 {
728 destination_pixel[channel] = (f32::from(source_pixel[channel]) * alpha
729 + f32::from(destination_pixel[channel]) * inverse)
730 .round()
731 .clamp(0.0, 255.0) as u8;
732 }
733 destination_pixel[3] = 255;
734 }
735 }
736}
737
738#[cfg(test)]
739mod tests {
740 use std::{
741 sync::{
742 Mutex as StdMutex,
743 atomic::{AtomicBool, Ordering as AtomicOrdering},
744 },
745 thread,
746 };
747
748 use super::*;
749
750 struct CapturingSink {
751 pp_log: PpLog,
752 received: Arc<StdMutex<Vec<MediaBuffer>>>,
753 }
754
755 impl Element for CapturingSink {
756 fn name(&self) -> Arc<str> {
757 "capture".into()
758 }
759
760 fn element_type(&self) -> ElementType {
761 ElementType::Other
762 }
763
764 fn pp_log(&self) -> &PpLog {
765 &self.pp_log
766 }
767
768 fn pp_log_mut(&mut self) -> &mut PpLog {
769 &mut self.pp_log
770 }
771 }
772
773 impl Sink for CapturingSink {
774 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
775 self.received.lock().unwrap().push(buf);
776 Ok(())
777 }
778
779 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
780 Ok(())
781 }
782 }
783
784 fn options(width: u32, height: u32) -> VideoCompositorOptions {
785 VideoCompositorOptions {
786 width,
787 height,
788 frame_rate: ffmpeg::Rational::new(30, 1),
789 background: Color::BLACK,
790 }
791 }
792
793 fn solid_frame(
794 width: u32,
795 height: u32,
796 color: Color,
797 ) -> Arc<UnboundObjectPoolRef<ffmpeg::frame::Video>> {
798 let pool = UnboundObjectPool::new(
799 0,
800 move || ffmpeg::frame::Video::new(ffmpeg::format::Pixel::BGRA, width, height),
801 |_| {},
802 );
803 let mut frame = pool.get();
804 fill_background(&mut frame, color);
805 Arc::new(frame)
806 }
807
808 fn pixel(frame: &ffmpeg::frame::Video, x: usize, y: usize) -> [u8; 4] {
809 let offset = y * frame.stride(0) + x * 4;
810 frame.data(0)[offset..offset + 4].try_into().unwrap()
811 }
812
813 fn input(
814 handle: &VideoCompositorHandle,
815 name: &str,
816 layer: VideoLayer,
817 ) -> (Box<dyn Sink>, VideoLayerHandle) {
818 let input = handle.add_source(name, layer).unwrap().unwrap();
819 (input.sink, input.layer)
820 }
821
822 #[test]
823 fn composes_inputs_in_z_order_and_preserves_output_contract() {
824 let (mut compositor, handle) = VideoCompositor::new("compositor", options(4, 4)).unwrap();
825 let mut background = VideoLayer::new(VideoRect::new(0, 0, 4, 4));
826 background.fit = VideoFit::Stretch;
827 let (mut red_sink, _) = input(&handle, "red", background);
828 let mut overlay = VideoLayer::new(VideoRect::new(1, 1, 2, 2));
829 overlay.z_index = 1;
830 overlay.fit = VideoFit::Stretch;
831 let (mut blue_sink, _) = input(&handle, "blue", overlay);
832 red_sink
833 .consume(MediaBuffer::Video(solid_frame(4, 4, Color::new(255, 0, 0))))
834 .unwrap();
835 blue_sink
836 .consume(MediaBuffer::Video(solid_frame(2, 2, Color::new(0, 0, 255))))
837 .unwrap();
838
839 let frame = compositor.compose_frame().unwrap();
840 assert_eq!(frame.format(), ffmpeg::format::Pixel::BGRA);
841 assert_eq!((frame.width(), frame.height()), (4, 4));
842 assert_eq!(frame.pts(), Some(0));
843 assert_eq!(pixel(&frame, 0, 0), [0, 0, 255, 255]);
844 assert_eq!(pixel(&frame, 1, 1), [255, 0, 0, 255]);
845 }
846
847 #[test]
848 fn layer_handle_moves_blends_and_hides_a_live_source() {
849 let (mut compositor, handle) = VideoCompositor::new("compositor", options(3, 1)).unwrap();
850 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
851 let (mut sink, layer_handle) = input(&handle, "white", layer);
852 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::WHITE)))
853 .unwrap();
854
855 layer_handle.set_rect(VideoRect::new(1, 0, 1, 1)).unwrap();
856 layer_handle.set_opacity(0.5).unwrap();
857 let blended = compositor.compose_frame().unwrap();
858 assert_eq!(pixel(&blended, 0, 0), [0, 0, 0, 255]);
859 assert_eq!(pixel(&blended, 1, 0), [128, 128, 128, 255]);
860
861 layer_handle.set_visible(false).unwrap();
862 let hidden = compositor.compose_frame().unwrap();
863 assert_eq!(pixel(&hidden, 1, 0), [0, 0, 0, 255]);
864 assert_eq!(hidden.pts(), Some(1));
865 }
866
867 #[test]
868 fn input_keeps_only_the_latest_frame() {
869 let (mut compositor, handle) = VideoCompositor::new("compositor", options(1, 1)).unwrap();
870 let (mut sink, _) = input(
871 &handle,
872 "latest",
873 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
874 );
875 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
876 .unwrap();
877 sink.consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 255, 0))))
878 .unwrap();
879
880 let frame = compositor.compose_frame().unwrap();
881 assert_eq!(pixel(&frame, 0, 0), [0, 255, 0, 255]);
882 }
883
884 #[test]
885 fn frame_replacement_and_composition_run_concurrently() {
886 let (mut compositor, handle) = VideoCompositor::new("compositor", options(1, 1)).unwrap();
887 let (mut sink, _) = input(&handle, "live", VideoLayer::new(VideoRect::new(0, 0, 1, 1)));
888 let red = solid_frame(1, 1, Color::new(255, 0, 0));
889 let green = solid_frame(1, 1, Color::new(0, 255, 0));
890 let done = Arc::new(AtomicBool::new(false));
891 let producer_done = done.clone();
892 let producer = thread::spawn(move || {
893 for index in 0..2_000 {
894 let frame = if index % 2 == 0 {
895 red.clone()
896 } else {
897 green.clone()
898 };
899 sink.consume(MediaBuffer::Video(frame)).unwrap();
900 }
901 sink.consume(MediaBuffer::Video(green)).unwrap();
904 producer_done.store(true, AtomicOrdering::Release);
905 });
906
907 while !done.load(AtomicOrdering::Acquire) {
908 let frame = compositor.compose_frame().unwrap();
909 assert!(matches!(
910 pixel(&frame, 0, 0),
911 [0, 0, 0, 255] | [0, 0, 255, 255] | [0, 255, 0, 255]
912 ));
913 }
914 producer.join().unwrap();
915 let final_frame = compositor.compose_frame().unwrap();
916 assert_eq!(pixel(&final_frame, 0, 0), [0, 255, 0, 255]);
917 }
918
919 #[test]
920 fn replacing_a_name_invalidates_old_sink_and_layer_handle() {
921 let (mut compositor, handle) = VideoCompositor::new("compositor", options(1, 1)).unwrap();
922 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
923 let (mut old_sink, old_layer) = input(&handle, "camera", layer);
924 let (mut new_sink, _) = input(&handle, "camera", layer);
925 assert!(matches!(
926 old_layer.set_visible(false),
927 Err(VideoCompositorError::SourceRemoved)
928 ));
929 old_sink
930 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(255, 0, 0))))
931 .unwrap();
932 new_sink
933 .consume(MediaBuffer::Video(solid_frame(1, 1, Color::new(0, 0, 255))))
934 .unwrap();
935
936 let frame = compositor.compose_frame().unwrap();
937 assert_eq!(pixel(&frame, 0, 0), [255, 0, 0, 255]);
938 assert_eq!(handle.source_count(), 1);
939 }
940
941 #[test]
942 fn stop_removes_only_the_current_registration() {
943 let (_compositor, handle) = VideoCompositor::new("compositor", options(1, 1)).unwrap();
944 let layer = VideoLayer::new(VideoRect::new(0, 0, 1, 1));
945 let (mut old_sink, _) = input(&handle, "camera", layer);
946 let (_new_sink, _) = input(&handle, "camera", layer);
947 old_sink.control(ControlMsg::Stop).unwrap();
948 assert_eq!(handle.source_count(), 1);
949
950 handle.remove_source("camera");
951 assert_eq!(handle.source_count(), 0);
952 }
953
954 #[test]
955 fn contain_and_cover_preserve_aspect_ratio() {
956 let rect = VideoRect::new(10, 20, 100, 100);
957 let contain = layer_geometry(160, 90, rect, VideoFit::Contain).unwrap();
958 assert_eq!((contain.image_width, contain.image_height), (100, 56));
959 assert_eq!((contain.image_x, contain.image_y), (10, 42));
960
961 let cover = layer_geometry(160, 90, rect, VideoFit::Cover).unwrap();
962 assert_eq!((cover.image_width, cover.image_height), (178, 100));
963 assert_eq!((cover.image_x, cover.image_y), (-29, 20));
964 }
965
966 #[test]
967 fn rejects_invalid_layers_and_non_video_buffers() {
968 let (_compositor, handle) = VideoCompositor::new("compositor", options(1, 1)).unwrap();
969 let invalid = VideoLayer {
970 opacity: 1.5,
971 ..VideoLayer::new(VideoRect::new(0, 0, 1, 1))
972 };
973 assert!(matches!(
974 handle.add_source("invalid", invalid),
975 Err(VideoCompositorError::InvalidOpacity(1.5))
976 ));
977
978 let (mut sink, _) = input(
979 &handle,
980 "valid",
981 VideoLayer::new(VideoRect::new(0, 0, 1, 1)),
982 );
983 let error = sink
984 .consume(MediaBuffer::Packet(Arc::new(ffmpeg::Packet::empty())))
985 .unwrap_err();
986 assert!(matches!(
987 error,
988 crate::Error::VideoCompositorError(VideoCompositorError::UnsupportedBuffer("Packet"))
989 ));
990 }
991
992 #[test]
993 fn pushes_fixed_format_frames_with_contiguous_pts() {
994 let (mut compositor, _) = VideoCompositor::new("compositor", options(2, 2)).unwrap();
995 let received = Arc::new(StdMutex::new(Vec::new()));
996 compositor.src_pads()[0].link(Box::new(CapturingSink {
997 received: received.clone(),
998 pp_log: element_pp_log(ElementType::Other, "capture", None),
999 }));
1000 let (bus, _) = Bus::new();
1001 compositor.push_frame(&bus).unwrap();
1002 compositor.push_frame(&bus).unwrap();
1003
1004 let received = received.lock().unwrap();
1005 let pts: Vec<_> = received
1006 .iter()
1007 .filter_map(|buffer| match buffer {
1008 MediaBuffer::Video(frame) => Some(frame.pts()),
1009 _ => None,
1010 })
1011 .collect();
1012 assert_eq!(pts, vec![Some(0), Some(1)]);
1013 }
1014
1015 struct TimestampSink {
1016 pp_log: PpLog,
1017 tx: crossbeam_channel::Sender<Instant>,
1018 }
1019
1020 impl Element for TimestampSink {
1021 fn name(&self) -> Arc<str> {
1022 "timestamp-recorder".into()
1023 }
1024 fn element_type(&self) -> ElementType {
1025 ElementType::Other
1026 }
1027 fn pp_log(&self) -> &PpLog {
1028 &self.pp_log
1029 }
1030 fn pp_log_mut(&mut self) -> &mut PpLog {
1031 &mut self.pp_log
1032 }
1033 }
1034
1035 impl Sink for TimestampSink {
1036 fn consume(&mut self, buf: MediaBuffer) -> Result<()> {
1037 if matches!(buf, MediaBuffer::Video(_)) {
1038 let _ = self.tx.send(Instant::now());
1039 }
1040 Ok(())
1041 }
1042 fn control(&mut self, _msg: ControlMsg) -> Result<()> {
1043 Ok(())
1044 }
1045 }
1046
1047 #[test]
1060 fn resuming_after_a_pause_preserves_output_phase() {
1061 use crate::pipeline::Pipeline;
1062
1063 let (tx, rx) = crossbeam_channel::unbounded();
1064 let sink = TimestampSink {
1065 tx,
1066 pp_log: element_pp_log(ElementType::Other, "timestamp-recorder", None),
1067 };
1068 let (compositor, _handle) = VideoCompositor::new(
1069 "compositor",
1070 VideoCompositorOptions {
1071 frame_rate: ffmpeg::Rational::new(10, 1),
1072 ..options(2, 2)
1073 },
1074 )
1075 .unwrap();
1076
1077 let pipeline = Pipeline::new("phase-test", compositor, |source, ctx| {
1078 let branch = ctx.branch().to(Box::new(sink))?;
1079 ctx.attach(source, 0, branch)?;
1080 Ok(())
1081 })
1082 .expect("test pipeline wiring must succeed");
1083
1084 pipeline.run();
1085 for _ in 0..2 {
1088 rx.recv_timeout(Duration::from_millis(500))
1089 .expect("expected steady frames before pausing");
1090 }
1091 pipeline.pause();
1092 thread::sleep(Duration::from_millis(500));
1093
1094 let resumed_at = Instant::now();
1095 pipeline.resume();
1096 let first_after_resume = rx
1097 .recv_timeout(Duration::from_millis(500))
1098 .expect("expected a frame after resume");
1099 pipeline.stop();
1100 pipeline.bus().log_events();
1101
1102 let gap = first_after_resume.saturating_duration_since(resumed_at);
1103 assert!(
1104 gap >= Duration::from_millis(50),
1105 "expected the post-pause frame to land close to a full 100ms \
1106 interval after resume (phase preserved from before the \
1107 pause), not almost immediately (phase reset to the resume \
1108 instant): got {gap:?}"
1109 );
1110 }
1111}